package com.tcshuo;

import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream;
import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream;
import org.apache.commons.compress.utils.IOUtils;
import org.json.JSONArray;
import org.json.JSONObject;

import java.io.*;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.text.DecimalFormat;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class DockerImageDownloader {

    private static final String DEFAULT_DOCKER_HUB_REGISTRY = "docker.1panel.live";
    private static final String DEFAULT_SCHEME = "https";
    private static final int BUFFER_SIZE = 1024 * 1024;
    private static final int DEFAULT_DOWNLOAD_THREADS = 2;
    private static final long SPEED_CALCULATION_INTERVAL_MS = 1000;
    private static final DecimalFormat df = new DecimalFormat("#.##");

    // 类成员变量
    private String registryHost;
    private String imageNameNoHost;
    private String imageNameInRepo;
    private String imageTag;
    private String architecture;
    private String authToken;

    private Path baseDownloadDir;
    private Path imageFilesystemDir;
    private Path blobsDir;
    private Path manifestFilePath;
    private Path configFilePath;

    private String originalFullImageName;
    private int downloadThreads;

    private final Map<String, Integer> threadLineMap = new ConcurrentHashMap<>();
    private final AtomicInteger nextLine = new AtomicInteger(0);
    // private int consoleLinesPrepared = 0; // 复杂的ANSI进度条已简化

    public DockerImageDownloader(String fullImageName, String arch, String token, int threads) throws MalformedURLException {
        this.originalFullImageName = fullImageName;
        parseFullImageName(fullImageName);
        this.architecture = (arch == null || arch.isEmpty()) ? null : arch;
        this.authToken = token;
        this.downloadThreads = (threads <= 0) ? DEFAULT_DOWNLOAD_THREADS : threads;

        String cacheDirName = imageNameInRepo.replace("/", "_") + "_" + imageTag;
        if (this.architecture != null && !this.architecture.isEmpty()) { // 确保 arch 不为空字符串
            cacheDirName += "_" + this.architecture;
        }
        this.baseDownloadDir = Paths.get(".", cacheDirName + "_cache").toAbsolutePath();
        this.imageFilesystemDir = baseDownloadDir.resolve("image_filesystem_merged");
        this.blobsDir = baseDownloadDir.resolve("blobs").resolve("sha256");
        this.manifestFilePath = baseDownloadDir.resolve("downloaded_manifest.json");
    }

    /**
     * 获取当前系统的 CPU 架构。
     * Java 的 `os.arch` 可能返回如 "x86_64", "amd64", "aarch64" 等。
     * Docker 架构通常使用 "amd64", "arm64", "arm/v7" 等。
     * 需要进行一些映射。
     *
     * @return Docker 兼容的架构字符串，或 null 如果无法确定。
     */
    private static String getCurrentSystemArch() {
        String osArch = System.getProperty("os.arch").toLowerCase();
        if (osArch.contains("amd64") || osArch.contains("x86_64")) {
            return "amd64";
        } else if (osArch.contains("aarch64") || osArch.contains("arm64")) {
            return "arm64";
        } else if (osArch.contains("arm")) { // 可能需要更细致的判断 armv7, armv6 等
            return "arm"; // 或者更具体的，如 "arm/v7" 如果能确定
        }
        // 添加更多架构的映射...
        System.out.println("警告: 未能自动检测到明确的 Docker 兼容 CPU 架构，系统返回: " + osArch);
        return null; // 返回 null，让后续逻辑决定是否报错或使用默认
    }


    private void parseFullImageName(String fullImageName) throws MalformedURLException {
        String tempImageName = fullImageName;
        String tempRegistryHost = null;
        String tempImageTag = "latest";

        int tagColonPos = tempImageName.lastIndexOf(':');
        if (tagColonPos > 0) {
            int lastSlashPos = tempImageName.lastIndexOf('/');
            if (lastSlashPos < tagColonPos) {
                tempImageTag = tempImageName.substring(tagColonPos + 1);
                tempImageName = tempImageName.substring(0, tagColonPos);
            }
        }

        int firstSlashPos = tempImageName.indexOf('/');
        String firstPart = (firstSlashPos > 0) ? tempImageName.substring(0, firstSlashPos) : tempImageName;

        if (firstSlashPos > 0 && (firstPart.contains(".") || firstPart.contains(":") || firstPart.equals("localhost"))) {
            tempRegistryHost = firstPart;
            this.imageNameInRepo = tempImageName.substring(firstSlashPos + 1);
            this.imageNameNoHost = this.imageNameInRepo;
        } else if (firstSlashPos == -1 && !firstPart.contains(".") && !firstPart.contains(":")) {
            tempRegistryHost = DEFAULT_DOCKER_HUB_REGISTRY;
            this.imageNameInRepo = "library/" + tempImageName;
            this.imageNameNoHost = tempImageName;
        } else if (firstSlashPos > 0 && !firstPart.contains(".") && !firstPart.contains(":")) {
            tempRegistryHost = DEFAULT_DOCKER_HUB_REGISTRY;
            this.imageNameInRepo = tempImageName;
            this.imageNameNoHost = tempImageName;
        } else {
            if (tempImageName.contains("/")) {
                String potentialHostInPath = tempImageName.substring(0, tempImageName.indexOf('/'));
                if (potentialHostInPath.contains(".")) {
                    tempRegistryHost = potentialHostInPath;
                    this.imageNameInRepo = tempImageName.substring(tempImageName.indexOf('/') + 1);
                    this.imageNameNoHost = this.imageNameInRepo;
                } else {
                    tempRegistryHost = DEFAULT_DOCKER_HUB_REGISTRY;
                    this.imageNameInRepo = tempImageName;
                    this.imageNameNoHost = tempImageName;
                }
            } else {
                tempRegistryHost = DEFAULT_DOCKER_HUB_REGISTRY;
                this.imageNameInRepo = "library/" + tempImageName;
                this.imageNameNoHost = tempImageName;
            }
        }
        this.registryHost = tempRegistryHost;
        this.imageTag = tempImageTag;

        System.out.println("解析镜像名结果:");
        System.out.println("  Registry 主机 (用于API): " + this.registryHost);
        System.out.println("  仓库中镜像路径 (用于API): " + this.imageNameInRepo);
        System.out.println("  本地标记用镜像名 (无主机): " + this.imageNameNoHost);
        System.out.println("  镜像标签: " + this.imageTag);
    }

    private String calculateFileSha256(Path filePath) throws IOException, NoSuchAlgorithmException {
        MessageDigest sha256 = MessageDigest.getInstance("SHA-256");
        try (InputStream fis = Files.newInputStream(filePath)) {
            byte[] buffer = new byte[BUFFER_SIZE];
            int bytesRead;
            while ((bytesRead = fis.read(buffer)) != -1) {
                sha256.update(buffer, 0, bytesRead);
            }
        }
        byte[] hashBytes = sha256.digest();
        StringBuilder hexString = new StringBuilder(2 * hashBytes.length);
        for (byte b : hashBytes) {
            String hex = Integer.toHexString(0xff & b);
            if (hex.length() == 1) hexString.append('0');
            hexString.append(hex);
        }
        return hexString.toString();
    }

    private HttpURLConnection createConnection(String urlString, String method, String acceptHeader) throws IOException {
        URL url = new URL(urlString);
        HttpURLConnection conn = (HttpURLConnection) url.openConnection();
        conn.setRequestMethod(method);
        conn.setConnectTimeout(30000);
        conn.setReadTimeout(60000 * 10);

        conn.setRequestProperty("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36");
        conn.setRequestProperty("Accept", (acceptHeader != null && !acceptHeader.isEmpty()) ? acceptHeader : "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9");
        conn.setRequestProperty("Accept-Language", "en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7");
        conn.setRequestProperty("Accept-Encoding", "gzip, deflate, br");
        conn.setRequestProperty("Connection", "keep-alive");
        conn.setRequestProperty("Upgrade-Insecure-Requests", "1");

        if (this.authToken != null && !this.authToken.isEmpty() && !"anonymous_token_placeholder".equalsIgnoreCase(this.authToken)) {
            conn.setRequestProperty("Authorization", "Bearer " + this.authToken);
        }
        return conn;
    }

    private String downloadToString(String urlString, String acceptHeader) throws IOException {
        HttpURLConnection conn = null;
        InputStream inputStream = null;
        BufferedReader reader = null;
        StringBuilder content = new StringBuilder();
        try {
            conn = createConnection(urlString, "GET", acceptHeader);
            int responseCode = conn.getResponseCode();

            if (responseCode == HttpURLConnection.HTTP_FORBIDDEN) {
                System.err.println("下载失败 (403 Forbidden - 可能被Cloudflare拦截): " + urlString);
                handleErrorStream(conn);
                throw new IOException("请求被服务器拒绝 (403 Forbidden) 访问 URL: " + urlString);
            } else if (responseCode == HttpURLConnection.HTTP_UNAUTHORIZED) { // 注意，这里不应该是 FORBIDDEN
                System.err.println("下载失败 (认证错误): " + urlString + ". 响应码: " + responseCode);
                handleErrorStream(conn);
                throw new IOException("认证失败: " + responseCode + " for " + urlString);
            } else if (responseCode != HttpURLConnection.HTTP_OK && responseCode != HttpURLConnection.HTTP_CREATED) {
                System.err.println("下载失败: " + urlString + ". 响应码: " + responseCode);
                handleErrorStream(conn);
                throw new IOException("HTTP 错误 " + responseCode + " 访问 URL: " + urlString);
            }

            String contentType = conn.getContentType();
            if (contentType != null && contentType.toLowerCase().contains("text/html") &&
                    !(acceptHeader != null && acceptHeader.toLowerCase().contains("text/html"))) { // 如果我们不期望html却收到了
                System.err.println("警告: 期望 JSON/Octet-stream 但收到 HTML。可能是 Cloudflare 拦截页面。URL: " + urlString);
            }

            InputStream responseStream = conn.getInputStream();
            String encoding = conn.getContentEncoding();
            if ("gzip".equalsIgnoreCase(encoding)) {
                responseStream = new GzipCompressorInputStream(responseStream);
            } else if ("deflate".equalsIgnoreCase(encoding)) {
                // responseStream = new DeflaterInputStream(responseStream); // Java 7+
            } else if ("br".equalsIgnoreCase(encoding)) {
                System.err.println("警告: 服务器发送了 Brotli (br) 编码的内容，Java 1.8 HttpURLConnection 可能无法自动处理。");
            }

            reader = new BufferedReader(new InputStreamReader(responseStream, StandardCharsets.UTF_8));
            String line;
            while ((line = reader.readLine()) != null) {
                content.append(line);
            }
            if (content.toString().toLowerCase().contains("<title>attention required! | cloudflare</title>")) {
                System.err.println("检测到 Cloudflare 拦截页面内容。");
                throw new IOException("请求被 Cloudflare 拦截，URL: " + urlString);
            }
            return content.toString();
        } finally {
            if (reader != null) try {
                reader.close();
            } catch (IOException ignore) {
            }
            if (inputStream != null) try {
                inputStream.close();
            } catch (IOException ignore) {
            }
            if (conn != null) conn.disconnect();
        }
    }

    private void handleErrorStream(HttpURLConnection conn) {
        try (InputStream errorStream = conn.getErrorStream()) {
            if (errorStream != null) {
                InputStream decompressedErrorStream = errorStream;
                String encoding = conn.getContentEncoding();
                if ("gzip".equalsIgnoreCase(encoding)) {
                    decompressedErrorStream = new GzipCompressorInputStream(errorStream);
                }
                try (BufferedReader errorReader = new BufferedReader(new InputStreamReader(decompressedErrorStream, StandardCharsets.UTF_8))) {
                    String errorLine;
                    System.err.println("服务器错误详情:");
                    while ((errorLine = errorReader.readLine()) != null) System.err.println(errorLine);
                }
            }
        } catch (IOException e) {
            System.err.println("读取错误流时发生异常: " + e.getMessage());
        }
    }

    private long downloadFileCallable(String urlString, Path outputPath, long totalSize, String layerIdentifier, String expectedDigestSha256)
            throws IOException, NoSuchAlgorithmException {
        if (Files.exists(outputPath)) {
            if (expectedDigestSha256 != null && !expectedDigestSha256.isEmpty()) {
                System.out.printf("[%s] 缓存文件存在: %s. 正在验证哈希...\n", Thread.currentThread().getName(), outputPath.getFileName());
                String existingFileHash = calculateFileSha256(outputPath);
                if (expectedDigestSha256.equalsIgnoreCase(existingFileHash)) {
                    System.out.printf("[%s] 哈希验证成功 (缓存命中): %s\n", Thread.currentThread().getName(), outputPath.getFileName());
                    Integer lineNum = threadLineMap.computeIfAbsent(layerIdentifier, k -> nextLine.getAndIncrement());
                    printIndividualProgress(Files.size(outputPath), Files.size(outputPath), layerIdentifier, 0.0, lineNum, true);
                    return Files.size(outputPath);
                } else {
                    System.out.printf("[%s] 哈希验证失败. 将重新下载: %s (现有: %s..., 期望: %s...)\n",
                            Thread.currentThread().getName(), outputPath.getFileName(),
                            existingFileHash.substring(0, Math.min(12, existingFileHash.length())),
                            expectedDigestSha256.substring(0, Math.min(12, expectedDigestSha256.length())));
                    Files.delete(outputPath);
                }
            } else {
                System.out.printf("[%s] 缓存文件存在但无期望哈希进行验证: %s. 将使用此缓存文件。\n", Thread.currentThread().getName(), outputPath.getFileName());
                return Files.size(outputPath);
            }
        }
        HttpURLConnection conn = null;
        InputStream inputStream = null;
        FileOutputStream outputStream = null;
        long downloadedBytes = 0;
        final SpeedCalcState speedState = new SpeedCalcState();
        Integer lineNum = threadLineMap.computeIfAbsent(layerIdentifier, k -> nextLine.getAndIncrement());
        try {
            printIndividualProgress(0, totalSize, layerIdentifier, 0.0, lineNum, false);
            conn = createConnection(urlString, "GET", "application/octet-stream");
            int responseCode = conn.getResponseCode();
            if (responseCode == HttpURLConnection.HTTP_FORBIDDEN) {
                System.err.printf("\n[%s] 下载 blob 失败 (403 Forbidden - 可能被Cloudflare拦截): %s\n", Thread.currentThread().getName(), outputPath.getFileName());
                handleErrorStream(conn);
                throw new IOException("请求被服务器拒绝 (403 Forbidden) 下载 blob: " + outputPath.getFileName());
            } else if (responseCode == HttpURLConnection.HTTP_UNAUTHORIZED) {
                System.err.printf("\n[%s] 下载 blob 失败 (认证错误) %s. 响应码: %d\n", Thread.currentThread().getName(), outputPath.getFileName(), responseCode);
                handleErrorStream(conn);
                throw new IOException("Blob 认证失败: " + responseCode + " for " + outputPath.getFileName());
            } else if (responseCode != HttpURLConnection.HTTP_OK && responseCode != HttpURLConnection.HTTP_ACCEPTED && responseCode != 307) {
                System.err.printf("\n[%s] 下载 blob 失败 %s. 响应码: %d\n", Thread.currentThread().getName(), outputPath.getFileName(), responseCode);
                handleErrorStream(conn);
                throw new IOException("HTTP 错误 " + responseCode + " 访问 URL: " + urlString);
            }
            if (responseCode == 307) {
                String redirectUrl = conn.getHeaderField("Location");
                if (redirectUrl == null)
                    throw new IOException("HTTP 307 重定向 " + urlString + " 但未找到 Location 头部。");
                System.out.printf("\n[%s] 重定向到: %s (层: %s)\n", Thread.currentThread().getName(), redirectUrl, layerIdentifier);
                conn.disconnect();
                URL newUrl = new URL(redirectUrl);
                conn = (HttpURLConnection) newUrl.openConnection();
                conn.setRequestMethod("GET");
                conn.setRequestProperty("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36");
                conn.setRequestProperty("Accept", "application/octet-stream,*/*");
                conn.setConnectTimeout(15000);
                conn.setReadTimeout(60000 * 10);
                responseCode = conn.getResponseCode();
                if (responseCode != HttpURLConnection.HTTP_OK) {
                    System.err.printf("\n[%s] 从重定向 URL 下载 blob 失败 %s. 响应码: %d (层: %s)\n", Thread.currentThread().getName(), redirectUrl, responseCode, layerIdentifier);
                    handleErrorStream(conn);
                    throw new IOException("HTTP 错误 " + responseCode + " 访问重定向 URL: " + redirectUrl);
                }
            }
            inputStream = conn.getInputStream();
            String encoding = conn.getContentEncoding();
            if ("gzip".equalsIgnoreCase(encoding)) inputStream = new GzipCompressorInputStream(inputStream);
            Files.createDirectories(outputPath.getParent());
            outputStream = new FileOutputStream(outputPath.toFile());
            byte[] buffer = new byte[BUFFER_SIZE];
            int bytesRead;
            while ((bytesRead = inputStream.read(buffer)) != -1) {
                outputStream.write(buffer, 0, bytesRead);
                downloadedBytes += bytesRead;
                long currentTime = System.currentTimeMillis();
                long timeElapsed = currentTime - speedState.lastUpdateTime;
                if (timeElapsed >= SPEED_CALCULATION_INTERVAL_MS) {
                    long bytesDelta = downloadedBytes - speedState.lastDownloadedBytes;
                    if (timeElapsed > 0)
                        speedState.instantSpeedMbps = (double) bytesDelta / (1024 * 1024) / (timeElapsed / 1000.0);
                    else speedState.instantSpeedMbps = 0;
                    printIndividualProgress(downloadedBytes, totalSize, layerIdentifier, speedState.instantSpeedMbps, lineNum, false);
                    speedState.lastUpdateTime = currentTime;
                    speedState.lastDownloadedBytes = downloadedBytes;
                }
            }
            long timeElapsedFinal = System.currentTimeMillis() - speedState.lastUpdateTime;
            long bytesDeltaFinal = downloadedBytes - speedState.lastDownloadedBytes;
            if (timeElapsedFinal > 0 && downloadedBytes > 0)
                speedState.instantSpeedMbps = (double) bytesDeltaFinal / (1024 * 1024) / (timeElapsedFinal / 1000.0);
            else if (downloadedBytes == 0 && timeElapsedFinal > 0) speedState.instantSpeedMbps = 0.0;
            printIndividualProgress(downloadedBytes, totalSize, layerIdentifier, speedState.instantSpeedMbps, lineNum, true);
            if (expectedDigestSha256 != null && !expectedDigestSha256.isEmpty()) {
                System.out.printf("\n[%s] 正在验证下载文件 %s 的哈希...\n", Thread.currentThread().getName(), outputPath.getFileName());
                String downloadedFileHash = calculateFileSha256(outputPath);
                if (!expectedDigestSha256.equalsIgnoreCase(downloadedFileHash)) {
                    Files.deleteIfExists(outputPath);
                    throw new IOException("下载文件哈希不匹配: " + outputPath.getFileName() +
                            " (期望: " + expectedDigestSha256.substring(0, Math.min(12, expectedDigestSha256.length())) + "..., " +
                            "实际: " + downloadedFileHash.substring(0, Math.min(12, downloadedFileHash.length())) + "...)");
                }
                System.out.printf("[%s] 下载文件哈希验证成功: %s\n", Thread.currentThread().getName(), outputPath.getFileName());
            }
            return downloadedBytes;
        } finally {
            if (outputStream != null) try {
                outputStream.close();
            } catch (IOException ignore) {
            }
            if (inputStream != null) try {
                inputStream.close();
            } catch (IOException ignore) {
            }
            if (conn != null) conn.disconnect();
        }
    }

    private static final class SpeedCalcState {
        long lastUpdateTime = System.currentTimeMillis();
        long lastDownloadedBytes = 0;
        double instantSpeedMbps = 0.0;
    }


    private synchronized void printIndividualProgress(long downloaded, long total, String identifier, double speedMbps, int lineNum, boolean finished) {
        int barWidth = 25;
        String progressLine;
        String speedStr = df.format(speedMbps) + " MB/s";
        String status = finished ? "完成  " : "下载中";
        if (finished && downloaded == 0 && total > 0) { // 检查缓存文件是否存在
            Path cachedLayerPath = blobsDir.resolve(identifier.substring(identifier.indexOf('(') + 1, identifier.indexOf(')')).replace("sha256:", "") + ".tar.gz");
            if (Files.exists(cachedLayerPath)) status = "缓存命中";
            else status = "等待";
        }
        if (finished && total <= 0 && downloaded > 0) status = "完成 (大小未知)";
        if (total <= 0) {
            progressLine = String.format("%s %s: %.2f MB @ %s",
                    status, identifier, (double) downloaded / (1024 * 1024), speedStr);
        } else {
            double percent = (total == 0) ? (finished ? 1.0 : 0.0) : (double) downloaded / total;
            int filledBar = (int) (barWidth * percent);
            StringBuilder bar = new StringBuilder("[");
            for (int i = 0; i < barWidth; i++)
                bar.append(i < filledBar ? "=" : (i == filledBar && !finished && filledBar < barWidth ? ">" : " "));
            bar.append("]");
            progressLine = String.format("%s %s: %s %3d%% (%.2f/%.2f MB) @ %s",
                    status, identifier, bar.toString(), (int) (percent * 100),
                    (double) downloaded / (1024 * 1024), (double) total / (1024 * 1024), speedStr);
        }
        System.out.println(String.format("[%s] %s", Thread.currentThread().getName(), progressLine));
    }

    private void prepareConsoleForProgress(int numberOfLayers) {
        System.out.println("准备开始下载 " + numberOfLayers + " 个层 (每个层进度将独立打印)...");
    }

    /**
     * 替换路径中在 Windows 上非法的字符，主要是冒号。
     */
    private String sanitizePathSegment(String segment) {
        // 将冒号替换为下划线或其他安全字符
        // 也可以移除其他 Windows 不允许的字符: < > " / \ | ? *
        return segment.replace(":", "_")
                .replace("<", "_lt_")
                .replace(">", "_gt_")
                .replace("\"", "_quot_")
                .replace("/", "_slash_") // 虽然 tar 通常用 /，但以防万一
                .replace("\\", "_bslash_")
                .replace("|", "_pipe_")
                .replace("?", "_q_")
                .replace("*", "_star_");
    }

    private void extractTarGz(Path tarGzFile, Path destinationDir) throws IOException {
        System.out.println("解压中: " + tarGzFile.getFileName() + " 到 " + destinationDir);
        try (FileInputStream fis = new FileInputStream(tarGzFile.toFile());
             BufferedInputStream bis = new BufferedInputStream(fis);
             GzipCompressorInputStream gzis = new GzipCompressorInputStream(bis);
             TarArchiveInputStream tis = new TarArchiveInputStream(gzis)) {

            TarArchiveEntry entry;
            while ((entry = tis.getNextTarEntry()) != null) {
                // 对 entry.getName() 中的每个路径段进行清理
                String originalEntryName = entry.getName();
                String[] segments = originalEntryName.split("/"); // tar 使用 / 作为路径分隔符
                StringBuilder sanitizedNameBuilder = new StringBuilder();
                for (int i = 0; i < segments.length; i++) {
                    sanitizedNameBuilder.append(sanitizePathSegment(segments[i]));
                    if (i < segments.length - 1) {
                        sanitizedNameBuilder.append(File.separator); // 使用系统特定的分隔符构建本地路径
                    }
                }
                String sanitizedEntryName = sanitizedNameBuilder.toString();
                Path entryPath = destinationDir.resolve(sanitizedEntryName);

                if (!entryPath.normalize().startsWith(destinationDir.normalize())) {
                    throw new IOException("Tar entry正在尝试解压到目标目录之外: " + originalEntryName + " (清理后: " + sanitizedEntryName + ")");
                }

                if (entry.isDirectory()) {
                    if (!Files.exists(entryPath)) {
                        Files.createDirectories(entryPath);
                    }
                } else {
                    File entryFile = entryPath.toFile(); // 用于获取清理后的文件名
                    String entryNameOnly = entryFile.getName(); // 这是清理后的文件名

                    if (entryNameOnly.startsWith(".wh.")) { // 白化文件名也是清理后的
                        Path parentDir = entryPath.getParent();
                        if (parentDir == null) parentDir = destinationDir;

                        if (entryNameOnly.equals(sanitizePathSegment(".wh..wh..opq"))) {
                            System.out.println("处理不透明白化目录 (opaque whiteout): " + parentDir + " (层 " + tarGzFile.getFileName() + " 使其不透明)");
                        } else {
                            String originalWhiteoutTargetName = entryNameOnly.substring(".wh.".length()); // 这是清理后的白化目标名
                            // 我们需要找到原始未清理的白化目标名，但由于白化文件本身的文件名已被清理，
                            // 如果白化目标名也包含非法字符，这里会比较麻烦。
                            // 假设白化目标名在 tar 中是合法的，或者说其清理后的名字就是我们要找的。
                            Path originalFilePath = parentDir.resolve(originalWhiteoutTargetName);

                            System.out.println("处理白化文件: " + originalEntryName + " -> 尝试删除 " + originalFilePath);
                            if (Files.exists(originalFilePath)) {
                                if (Files.isDirectory(originalFilePath)) {
                                    deleteDirectoryRecursively(originalFilePath);
                                } else {
                                    Files.delete(originalFilePath);
                                }
                                System.out.println("已删除 (白化): " + originalFilePath);
                            }
                        }
                        continue;
                    }

                    Path parentDirForFile = entryPath.getParent();
                    if (parentDirForFile != null && !Files.exists(parentDirForFile)) {
                        Files.createDirectories(parentDirForFile);
                    }
                    try (OutputStream out = Files.newOutputStream(entryPath)) {
                        IOUtils.copy(tis, out);
                    }
                }
            }
        }
    }

    private void deleteDirectoryRecursively(Path path) throws IOException {
        // ... (与之前版本相同)
        if (Files.isDirectory(path)) {
            Files.walk(path).sorted(Comparator.reverseOrder()).map(Path::toFile)
                    .forEach(file -> {
                        if (!file.delete()) System.err.println("删除失败 (递归中): " + file.getAbsolutePath());
                    });
        } else Files.deleteIfExists(path);
    }

    public void downloadImage() throws IOException, InterruptedException, ExecutionException, NoSuchAlgorithmException {
        System.out.println("开始下载镜像: " + this.registryHost + "/" + this.imageNameInRepo + ":" + this.imageTag +
                (architecture != null ? " (请求架构: " + architecture + ")" : " (未指定架构)"));
        Files.createDirectories(baseDownloadDir);
        Files.createDirectories(imageFilesystemDir);
        Files.createDirectories(blobsDir);

        System.out.println("--- 1. 获取 Manifest ---");
        String manifestListAcceptHeader = "application/vnd.docker.distribution.manifest.list.v2+json, application/vnd.oci.image.index.v1+json";
        String imageManifestAcceptHeader = "application/vnd.docker.distribution.manifest.v2+json, application/vnd.oci.image.manifest.v1+json";
        String currentManifestUrl = DEFAULT_SCHEME + "://" + registryHost + "/v2/" + imageNameInRepo + "/manifests/" + imageTag;

        if (registryHost.equals(DEFAULT_DOCKER_HUB_REGISTRY) && (authToken == null || authToken.isEmpty() || "anonymous_token_placeholder".equalsIgnoreCase(authToken))) {
            System.out.println("检测到 Docker Hub 且无有效Token，尝试获取匿名只读Token...");
            String tokenUrl = "https://auth.docker.io/token?service=registry.docker.io&scope=repository:" + imageNameInRepo + ":pull";
            try {
                String tokenResponse = downloadToString(tokenUrl, null);
                JSONObject tokenJson = new JSONObject(tokenResponse);
                this.authToken = tokenJson.getString("token");
                System.out.println("Docker Hub 匿名Token获取成功。");
            } catch (Exception e) {
                System.err.println("警告: 获取 Docker Hub 匿名Token失败: " + e.getMessage());
                this.authToken = "anonymous_token_placeholder";
            }
        }

        String manifestContent;
        JSONObject finalImageManifestJson;
        try {
            System.out.println("尝试获取 Manifest (可能为列表类型): " + currentManifestUrl);
            // 同时请求两种 manifest 类型，让服务器选择
            manifestContent = downloadToString(currentManifestUrl, manifestListAcceptHeader + ", " + imageManifestAcceptHeader);
            JSONObject initialJson = new JSONObject(manifestContent);

            if (initialJson.has("manifests")) { // 这是 Manifest List
                System.out.println("发现多架构 Manifest List。");
                String targetArch = this.architecture;
                if (targetArch == null || targetArch.isEmpty()) {
                    targetArch = getCurrentSystemArch();
                    if (targetArch != null) {
                        System.out.println("未指定架构，尝试使用当前系统架构: " + targetArch);
                        this.architecture = targetArch; // 更新类成员的架构，以便缓存目录名一致
                        // 需要重新生成 baseDownloadDir 等路径，如果架构改变了
                        String cacheDirName = imageNameInRepo.replace("/", "_") + "_" + imageTag + "_" + targetArch;
                        this.baseDownloadDir = Paths.get(".", cacheDirName + "_cache").toAbsolutePath();
                        this.imageFilesystemDir = baseDownloadDir.resolve("image_filesystem_merged");
                        this.blobsDir = baseDownloadDir.resolve("blobs").resolve("sha256");
                        this.manifestFilePath = baseDownloadDir.resolve("downloaded_manifest.json");
                        Files.createDirectories(this.baseDownloadDir); // 确保新路径存在
                        Files.createDirectories(this.imageFilesystemDir);
                        Files.createDirectories(this.blobsDir);
                    } else {
                        System.err.println("警告: 无法自动检测系统架构，且未指定架构。将尝试列表中的第一个兼容Linux的清单。");
                    }
                }

                JSONArray manifestsArray = initialJson.getJSONArray("manifests");
                String archSpecificDigest = null;
                if (targetArch != null) {
                    for (int i = 0; i < manifestsArray.length(); i++) {
                        JSONObject archManifestDesc = manifestsArray.getJSONObject(i);
                        if (archManifestDesc.has("platform")) {
                            JSONObject platform = archManifestDesc.getJSONObject("platform");
                            String manifestArch = platform.optString("architecture");
                            String manifestOs = platform.optString("os", "linux"); // 默认为 linux
                            if (targetArch.equals(manifestArch) && "linux".equals(manifestOs)) {
                                archSpecificDigest = archManifestDesc.getString("digest");
                                System.out.println("找到匹配架构 " + targetArch + " (os: linux) 的 Manifest Digest: " + archSpecificDigest);
                                break;
                            }
                        }
                    }
                }

                if (archSpecificDigest == null) { // 如果未指定架构或找不到匹配的，尝试一个默认策略
                    System.out.println("未找到完全匹配的架构或未指定架构。尝试查找列表中的第一个 linux/amd64 或 linux/arm64，或者任意linux。");
                    for (int i = 0; i < manifestsArray.length(); i++) { // 优先 amd64
                        JSONObject archManifestDesc = manifestsArray.getJSONObject(i);
                        if (archManifestDesc.has("platform") && "linux".equals(archManifestDesc.getJSONObject("platform").optString("os", "linux")) &&
                                "amd64".equals(archManifestDesc.getJSONObject("platform").optString("architecture"))) {
                            archSpecificDigest = archManifestDesc.getString("digest");
                            this.architecture = "amd64"; // 更新使用的架构
                            System.out.println("默认选择 linux/amd64 Manifest Digest: " + archSpecificDigest);
                            break;
                        }
                    }
                    if (archSpecificDigest == null) { // 其次 arm64
                        for (int i = 0; i < manifestsArray.length(); i++) {
                            JSONObject archManifestDesc = manifestsArray.getJSONObject(i);
                            if (archManifestDesc.has("platform") && "linux".equals(archManifestDesc.getJSONObject("platform").optString("os", "linux")) &&
                                    "arm64".equals(archManifestDesc.getJSONObject("platform").optString("architecture"))) {
                                archSpecificDigest = archManifestDesc.getString("digest");
                                this.architecture = "arm64";
                                System.out.println("默认选择 linux/arm64 Manifest Digest: " + archSpecificDigest);
                                break;
                            }
                        }
                    }
                    if (archSpecificDigest == null) { // 最后任意 linux
                        for (int i = 0; i < manifestsArray.length(); i++) {
                            JSONObject archManifestDesc = manifestsArray.getJSONObject(i);
                            if (archManifestDesc.has("platform") && "linux".equals(archManifestDesc.getJSONObject("platform").optString("os", "linux"))) {
                                archSpecificDigest = archManifestDesc.getString("digest");
                                this.architecture = archManifestDesc.getJSONObject("platform").optString("architecture");
                                System.out.println("默认选择列表中的第一个 Linux Manifest (" + this.architecture + "): " + archSpecificDigest);
                                break;
                            }
                        }
                    }
                }


                if (archSpecificDigest != null) {
                    currentManifestUrl = DEFAULT_SCHEME + "://" + registryHost + "/v2/" + imageNameInRepo + "/manifests/" + archSpecificDigest;
                    System.out.println("正在获取特定架构 (" + this.architecture + ") 的Image Manifest: " + currentManifestUrl);
                    manifestContent = downloadToString(currentManifestUrl, imageManifestAcceptHeader);
                    finalImageManifestJson = new JSONObject(manifestContent);
                } else {
                    throw new IOException("无法从 Manifest List 中找到合适的 Linux 架构的 manifest。");
                }
            } else if (initialJson.has("config") && initialJson.has("layers")) { // 直接是 Image Manifest
                System.out.println("获取到的是单一架构 Image Manifest。");
                finalImageManifestJson = initialJson;
                // 如果用户指定了架构，可以验证一下是否匹配
                if (this.architecture != null && !this.architecture.isEmpty() && finalImageManifestJson.has("architecture")) {
                    String imageManifestArch = finalImageManifestJson.optString("architecture");
                    if (!this.architecture.equals(imageManifestArch)) {
                        System.err.println("警告: 请求的架构 '" + this.architecture + "' 与镜像 Manifest 中的架构 '" + imageManifestArch + "' 不符。");
                    }
                } else if (this.architecture == null && finalImageManifestJson.has("architecture")) {
                    this.architecture = finalImageManifestJson.optString("architecture"); // 从 manifest 中获取架构
                    System.out.println("从 Image Manifest 中检测到架构: " + this.architecture);
                }
            } else {
                throw new IOException("无法识别的 Manifest 格式。内容: " + manifestContent.substring(0, Math.min(500, manifestContent.length())));
            }
        } catch (IOException e) {
            System.err.println("获取 Manifest 失败。URL: " + currentManifestUrl);
            throw e;
        }
        try (PrintWriter out = new PrintWriter(manifestFilePath.toFile(), "UTF-8")) {
            out.println(finalImageManifestJson.toString(4));
        }
        System.out.println("最终处理的镜像 Manifest 已保存到: " + manifestFilePath);

        System.out.println("--- 2. 下载 Config Blob ---");
        JSONObject configInfo = finalImageManifestJson.getJSONObject("config");
        String configDigestFull = configInfo.getString("digest");
        String configDigestContent = configDigestFull.replace("sha256:", "");
        long configSize = configInfo.optLong("size", -1);
        String configUrl = DEFAULT_SCHEME + "://" + registryHost + "/v2/" + imageNameInRepo + "/blobs/" + configDigestFull;
        this.configFilePath = blobsDir.resolve(configDigestContent + ".json");
        downloadFileCallable(configUrl, this.configFilePath, configSize, "Config Blob", configDigestContent);
        System.out.println("Config blob 已保存到: " + this.configFilePath);

        System.out.println("--- 3. 多线程下载 Layers ---");
        JSONArray layersArray = finalImageManifestJson.getJSONArray("layers");
        System.out.println("总计层数: " + layersArray.length() + ", 将使用 " + this.downloadThreads + " 个线程并发下载。");
        prepareConsoleForProgress(Math.min(layersArray.length(), this.downloadThreads));

        ExecutorService executor = Executors.newFixedThreadPool(this.downloadThreads);
        List<Future<DownloadedLayerInfo>> downloadFutures = new ArrayList<>();
        final AtomicLong totalImageDownloadSize = new AtomicLong(0);
        final List<Long> layerSizes = new ArrayList<>();
        for (int i = 0; i < layersArray.length(); i++) {
            JSONObject layerInfo = layersArray.getJSONObject(i);
            long currentLayerSize = layerInfo.optLong("size", -1);
            layerSizes.add(currentLayerSize);
            if (currentLayerSize > 0) totalImageDownloadSize.addAndGet(currentLayerSize);
        }
        System.out.println("预估总下载大小: " + (totalImageDownloadSize.get() > 0 ? String.format("%.2f MB", (double) totalImageDownloadSize.get() / (1024 * 1024)) : "未知"));
        final AtomicLong overallDownloadedBytes = new AtomicLong(0);
        final Object overallProgressLock = new Object();

        for (int i = 0; i < layersArray.length(); i++) {
            JSONObject layerInfo = layersArray.getJSONObject(i);
            final String layerDigestFull = layerInfo.getString("digest");
            final String layerDigestContent = layerDigestFull.replace("sha256:", "");
            final long layerSize = layerSizes.get(i);
            final String layerIdentifier = String.format("层 %d/%d (%s)", i + 1, layersArray.length(), layerDigestContent.substring(0, 12));
            Callable<DownloadedLayerInfo> downloadTask = new Callable<DownloadedLayerInfo>() {
                @Override
                public DownloadedLayerInfo call() throws Exception {
                    Path layerTarGzPath = blobsDir.resolve(layerDigestContent + ".tar.gz");
                    long downloaded = downloadFileCallable(
                            DEFAULT_SCHEME + "://" + registryHost + "/v2/" + imageNameInRepo + "/blobs/" + layerDigestFull,
                            layerTarGzPath, layerSize, layerIdentifier, layerDigestContent);
                    if (layerSize > 0) {
                        long currentOverall = overallDownloadedBytes.addAndGet(downloaded);
                        synchronized (overallProgressLock) {
                            System.out.printf("\n[主控] 总体已下载: %.2f MB / %.2f MB (%.2f%%)\n",
                                    (double) currentOverall / (1024 * 1024),
                                    (totalImageDownloadSize.get() > 0 ? (double) totalImageDownloadSize.get() / (1024 * 1024) : 0.0),
                                    (totalImageDownloadSize.get() > 0 ? (double) currentOverall * 100 / totalImageDownloadSize.get() : 0.0));
                        }
                    } else if (downloaded > 0) {
                        overallDownloadedBytes.addAndGet(downloaded);
                        synchronized (overallProgressLock) {
                            System.out.printf("\n[主控] 总体已下载: %.2f MB (部分层大小未知)\n", (double) overallDownloadedBytes.get() / (1024 * 1024));
                        }
                    }
                    return new DownloadedLayerInfo(layerTarGzPath, layerDigestFull);
                }
            };
            downloadFutures.add(executor.submit(downloadTask));
        }
        executor.shutdown();
        List<DownloadedLayerInfo> downloadedLayers = new ArrayList<>();
        boolean allDownloadsSuccessful = true;
        for (Future<DownloadedLayerInfo> future : downloadFutures) {
            try {
                downloadedLayers.add(future.get());
            } catch (InterruptedException | ExecutionException e) {
                System.err.println("一个层下载任务失败或被中断: " + e.getMessage());
                allDownloadsSuccessful = false;
                if (e.getCause() != null) e.getCause().printStackTrace(System.err);
                else e.printStackTrace(System.err);
            }
        }
        System.out.println("\n\n");
        if (!allDownloadsSuccessful) {
            System.err.println("由于部分层下载失败，后续的解压和打包操作将被跳过。");
            throw new IOException("部分层文件下载失败，请检查之前的错误日志。");
        }

        System.out.println("\n--- 4. 串行解压所有已下载的 Layers ---");
        for (int i = 0; i < layersArray.length(); i++) {
            String manifestLayerDigest = layersArray.getJSONObject(i).getString("digest");
            DownloadedLayerInfo layerToExtract = null;
            for (DownloadedLayerInfo downloaded : downloadedLayers) {
                if (downloaded.digest.equals(manifestLayerDigest)) {
                    layerToExtract = downloaded;
                    break;
                }
            }
            if (layerToExtract != null && Files.exists(layerToExtract.path)) {
                System.out.printf("\n解压层 (来自 manifest 顺序 %d): %s\n", i + 1, layerToExtract.digest.substring(7, 19));
                extractTarGz(layerToExtract.path, imageFilesystemDir);
            } else {
                System.err.println("严重警告: 未找到已下载的层文件对应 Manifest 中的 Digest: " + manifestLayerDigest +
                        "，或文件 '" + (layerToExtract != null ? layerToExtract.path : "未知路径") + "' 不存在。镜像将不完整。");
            }
        }
        System.out.println("--- 镜像所有组件下载和解压完成 ---");
        System.out.println("合并后的文件系统 (用于检查，非最终tar包内容) 位于: " + imageFilesystemDir);

    }

    public void createFinalTar(String outputTarName) throws IOException {
        System.out.println("--- 创建用于 'docker load' 的 Tar 包: " + outputTarName + " ---");
        Path outputTarPath = Paths.get(outputTarName).toAbsolutePath();
        JSONObject downloadedImageManifest = new JSONObject(new String(Files.readAllBytes(manifestFilePath), StandardCharsets.UTF_8));
        String configDigestFromFile = downloadedImageManifest.getJSONObject("config").getString("digest");
        String configFilenameInTar = configDigestFromFile.replace("sha256:", "") + ".json";
        try (FileOutputStream fos = new FileOutputStream(outputTarPath.toFile());
             BufferedOutputStream bos = new BufferedOutputStream(fos);
             TarArchiveOutputStream taos = new TarArchiveOutputStream(bos)) {
            taos.setLongFileMode(TarArchiveOutputStream.LONGFILE_GNU);
            taos.setBigNumberMode(TarArchiveOutputStream.BIGNUMBER_STAR);
            System.out.println("构建并添加顶层 manifest.json (供 docker load 使用) 到 tar 包");
            JSONObject loadManifestEntry = new JSONObject();
            loadManifestEntry.put("Config", configFilenameInTar);
            JSONArray repoTags = new JSONArray();
            repoTags.put(this.imageNameNoHost + ":" + this.imageTag);
            loadManifestEntry.put("RepoTags", repoTags);
            JSONArray layerFilesForLoadManifest = new JSONArray();
            JSONArray layersFromDownloadedManifest = downloadedImageManifest.getJSONArray("layers");
            for (int i = 0; i < layersFromDownloadedManifest.length(); i++) {
                String layerDigest = layersFromDownloadedManifest.getJSONObject(i).getString("digest");
                String layerFilenameInTar = "blobs/sha256/" + layerDigest.replace("sha256:", "") + ".tar.gz";
                layerFilesForLoadManifest.put(layerFilenameInTar);
            }
            loadManifestEntry.put("Layers", layerFilesForLoadManifest);
            JSONArray topLevelLoadManifest = new JSONArray();
            topLevelLoadManifest.put(loadManifestEntry);
            byte[] loadManifestBytes = topLevelLoadManifest.toString(4).getBytes(StandardCharsets.UTF_8);
            TarArchiveEntry manifestEntry = new TarArchiveEntry("manifest.json");
            manifestEntry.setSize(loadManifestBytes.length);
            taos.putArchiveEntry(manifestEntry);
            taos.write(loadManifestBytes);
            taos.closeArchiveEntry();

            System.out.println("添加 config 文件 (" + configFilenameInTar + ") 到 tar 包");
            // configFilePath 是 blobsDir 下的 <config_digest_content>.json
            if (Files.exists(this.configFilePath)) {
                addFileToTar(taos, this.configFilePath, configFilenameInTar);
            } else {
                System.err.println("错误: Config 文件未找到，无法添加到 tar: " + this.configFilePath);
                throw new FileNotFoundException("Config 文件未找到: " + this.configFilePath);
            }


            System.out.println("添加所有层文件到 tar 包的 'blobs/sha256/' 目录 (从缓存 " + blobsDir + ")");
            if (Files.exists(blobsDir) && Files.isDirectory(blobsDir)) {
                Files.list(blobsDir)
                        .filter(path -> path.toString().endsWith(".tar.gz"))
                        .forEach(layerTarGzPath -> {
                            try {
                                String entryNameInTar = "blobs/sha256/" + layerTarGzPath.getFileName().toString();
                                System.out.println("添加层: " + entryNameInTar);
                                addFileToTar(taos, layerTarGzPath, entryNameInTar);
                            } catch (IOException e) {
                                throw new UncheckedIOException("添加层文件到 tar 失败: " + layerTarGzPath, e);
                            }
                        });
            } else System.err.println("警告: blobs 目录 (" + blobsDir + ") 不存在或不是一个目录，无法添加层文件。");
            System.out.println("正在完成 tar 包...");
        }
        System.out.println("Tar 包已创建: " + outputTarPath.toAbsolutePath());
        System.out.println("注意: 此 tar 包的层结构为直接包含 .tar.gz blobs。");
    }

    private void addFileToTar(TarArchiveOutputStream taos, Path filePath, String entryName) throws IOException {
        File file = filePath.toFile();
        if (!file.exists()) {
            System.err.println("警告: 文件不存在，无法添加到 tar: " + filePath + " (目标条目名: " + entryName + ")");
            return;
        }
        TarArchiveEntry entry = new TarArchiveEntry(file, entryName);
        taos.putArchiveEntry(entry);
        try (FileInputStream fis = new FileInputStream(file)) {
            IOUtils.copy(fis, taos);
        }
        taos.closeArchiveEntry();
    }

    public void cleanupCache() throws IOException {
        System.out.println("--- 清理缓存目录: " + baseDownloadDir + " ---");
        if (Files.exists(baseDownloadDir)) {
            deleteDirectoryRecursively(baseDownloadDir);
            System.out.println("缓存目录已删除。");
        } else System.out.println("未找到缓存目录，无需清理。");
    }

    private void importToDocker(String tarFilePath) {
        System.out.println("--- 尝试将镜像导入到本地 Docker ---");
        Path tarFileAbsolutePath = Paths.get(tarFilePath).toAbsolutePath();
        if (!Files.exists(tarFileAbsolutePath)) {
            System.err.println("错误: Tar 包文件不存在，无法导入: " + tarFileAbsolutePath);
            return;
        }
        System.out.println("将要执行命令: docker load -i \"" + tarFileAbsolutePath + "\"");
        try {
            List<String> command = new ArrayList<>();
            command.add("docker");
            command.add("load");
            command.add("-i");
            command.add(tarFileAbsolutePath.toString());
            ProcessBuilder processBuilder = new ProcessBuilder(command);
            processBuilder.redirectErrorStream(true);
            Process process = processBuilder.start();
            System.out.println("Docker load 输出:");
            try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8))) {
                String line;
                while ((line = reader.readLine()) != null) System.out.println(line);
            }
            int exitCode = process.waitFor();
            if (exitCode == 0) {
                System.out.println("镜像成功导入到 Docker。");
                System.out.println("你可以使用 'docker images' 查看，并用 'docker run " +
                        this.imageNameNoHost + ":" + this.imageTag + " ...' 来运行。");
            } else System.err.println("Docker 导入失败，退出码: " + exitCode);
        } catch (IOException | InterruptedException e) {
            System.err.println("执行 docker load 命令时出错: " + e.getMessage());
            e.printStackTrace();
            if (e instanceof InterruptedException) Thread.currentThread().interrupt();
        }
    }

    public static void main(String[] args) {
        if (args.length < 1) {
            System.err.println("用法: java -jar dp.jar <完整镜像名> [--arch <架构>] [--token <Bearer Token>] [--threads <数量>] [--output-tar <输出tar包名.tar>] [--no-cleanup] ");
            System.err.println("示例 (私有仓库): java -jar dp.jar myregistry.com/namespace/image:tag --token YOUR_TOKEN_HERE --threads 8 --output-tar myimage.tar ");
            System.err.println("示例 (Docker Hub 公共): java -jar dp.jar ubuntu:latest --arch amd64 --threads 2");
            System.err.println("示例 (最简): java -jar dp.jar ubuntu:latest");
            System.exit(1);
        }
        String fullImageName = null;
        String architecture = null;
        String token = null;
        String outputTarName = null;
        boolean cleanup = true;
        int threads = DEFAULT_DOWNLOAD_THREADS;

        for (int i = 0; i < args.length; i++) {
            if (i == 0) fullImageName = args[i];
            else if (args[i].equals("--arch") && i + 1 < args.length) architecture = args[++i];
            else if (args[i].equals("--token") && i + 1 < args.length) token = args[++i];
            else if (args[i].equals("--threads") && i + 1 < args.length) {
                try {
                    threads = Integer.parseInt(args[++i]);
                    if (threads <= 0) {
                        System.err.println("警告: 线程数必须大于0，将使用默认值: " + DEFAULT_DOWNLOAD_THREADS);
                        threads = DEFAULT_DOWNLOAD_THREADS;
                    }
                } catch (NumberFormatException e) {
                    System.err.println("警告: 无效的线程数 '" + args[i] + "'，将使用默认值: " + DEFAULT_DOWNLOAD_THREADS);
                    threads = DEFAULT_DOWNLOAD_THREADS;
                }
            } else if (args[i].equals("--output-tar") && i + 1 < args.length) outputTarName = args[++i];
            else if (args[i].equals("--no-cleanup")) cleanup = false;
            else if (args[i].startsWith("--") && i > 0) System.err.println("警告: 未识别的选项: " + args[i]);
        }

        if (fullImageName == null) {
            System.err.println("错误: 未能解析出 Docker 镜像名。请确保它是第一个参数。");
            System.exit(1);
        }
        if (token == null || token.isEmpty())
            System.out.println("提示: 未提供 Bearer Token。对于需要认证的私有仓库，这可能导致下载失败。对于 Docker Hub 上的公共镜像，程序将尝试获取匿名访问Token。");

        DockerImageDownloader downloader = null;
        // 全局的程序是否成功的标志
        boolean operationSuccessful = false;
        try {
            downloader = new DockerImageDownloader(fullImageName, architecture, token, threads);
            downloader.downloadImage();
            if (outputTarName == null || outputTarName.isEmpty()) {
                String safeImageName = downloader.imageNameNoHost.replace("/", "_") + "_" + downloader.imageTag;
                if (downloader.architecture != null && !downloader.architecture.isEmpty())
                    safeImageName += "_" + downloader.architecture;
                outputTarName = safeImageName + ".tar";
            }
            downloader.createFinalTar(outputTarName);

            Scanner scanner = new Scanner(System.in);
            System.out.print("是否将镜像 '" + outputTarName + "' 导入到本地 Docker? (输入 'y' 或 'yes' 进行导入): ");
            String userInput = scanner.nextLine().trim().toLowerCase();
            if (userInput.equals("y") || userInput.equals("yes")) {
                downloader.importToDocker(outputTarName);
            } else {
                System.out.println("跳过 Docker 导入操作。");
            } ;
            scanner.close();
            operationSuccessful = true; // 标记所有主要操作成功
            System.out.println("操作成功完成。");

        } catch (Exception e) { // 捕获所有类型的异常
            System.err.println("发生严重错误: " + e.getMessage());
            e.printStackTrace(System.err);
            System.exit(1); // 错误发生，直接退出
        } finally {
            // 清理逻辑现在依赖于 operationSuccessful 和 cleanup 标志
            if (downloader != null) {
                if (operationSuccessful && cleanup) {
                    try {
                        downloader.cleanupCache();
                    } catch (IOException cleanupEx) {
                        System.err.println("清理缓存时发生错误: " + cleanupEx.getMessage());
                    }
                } else if (!operationSuccessful) {
                    System.out.println("由于发生错误，缓存未被清理，以便下次尝试。缓存位于: " + downloader.baseDownloadDir);
                } else { // operationSuccessful && !cleanup
                    System.out.println("跳过缓存清理 (用户指定 --no-cleanup)。缓存位于: " + downloader.baseDownloadDir);
                }
            }
        }
    }

    /**
     * 内部类，用于保存已下载的层文件的信息，包括其本地路径和原始摘要。
     * 这有助于在多线程下载完成后，能正确地按顺序解压和处理层。
     */
    private static class DownloadedLayerInfo {
        Path path;      // 下载的 .tar.gz 文件在本地缓存中的路径
        String digest;  // 该层的完整 digest (例如 "sha256:abcdef123...")

        public DownloadedLayerInfo(Path path, String digest) {
            this.path = path;
            this.digest = digest;
        }


        public Path getPath() {
            return path;
        }

        public String getDigest() {
            return digest;
        }

        @Override
        public String toString() {
            // 提供一个有用的字符串表示，主要用于调试
            return "DownloadedLayerInfo{" +
                    "path=" + (path != null ? path.toString() : "null") +
                    ", digest='" + (digest != null ? digest : "null") + '\'' +
                    '}';
        }
    }
}